/*
  Copyright 1996-2003
  Simon Whiteside

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

  $Id: TreeNodeObject.java,v 1.26 2004/10/11 15:04:21 sdw Exp $
*/
package simkin;
import java.util.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.IOException;
import java.io.StringWriter;

/**
 * This class provides and Executable interface to a TreeNode
 *<p>The method getValue, setValue and method all search for matching child labels within the TreeNode object. Only the first matching tag is used.  
 */
public class TreeNodeObject extends ExecutableRoot {
    /** A string indicating the location */
    String m_Location="";
    /**
     * the underlying treenode itself
     */
    TreeNode m_Node=null;
    /**
     * This caches the parse trees for the methods already come across
     */
    Hashtable m_MethodCache=null;
    /**
     * this variable controls whether new items are added to this element if they are not found, by default it is false,
but can be modified using the setAddIfNotPresent() method
    */
    boolean m_AddIfNotPresent=false;

    /**
     * Default Constructor 
     */
    public TreeNodeObject(){
	m_Node=new TreeNode();
    }
    /**
     * Constructor 
     * @param location location of this node
     * @param node the treenode to be stored
     */
    public TreeNodeObject(String location,TreeNode node){
	m_Location=location;
	setNode(node);
    }
    /**
     * Constructor 
     * @param location location of this node
     * @param in a stream containing the node
     */
    public TreeNodeObject(String location,InputStream in) throws IOException{
	load(location,in);
    }
    /** 
     * sets the node and clears the method cache
     */
    public void setNode(TreeNode node){
	m_Node=node;
	m_MethodCache=null;
    }
    /**
     * @return the value of the treenode data as a string
     */
    public String toString(){
	return m_Node.data();
    }
    /**
     * @return the value of the treenode data as a string
     */
    public int numChildren(){
	return m_Node.numChildren();
    }
    /**
     * @return true if the data in both nodes is the same
     */
    public boolean equals(Object o){
	return toString().equals(o.toString());
    }
    /**
     * @return the underlying treenode
     */
    public TreeNode getNode(){
	return m_Node;
    }
    /**
     * this method stores a value in the in-memory treenode object. If the value is of treenode type, the treenode is first copied and then stored. If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present
     * @param s the name of the value
     * @param v the value itself
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     * @throws FieldNotSupportedException - if the field could not be found
     */
    public void setValue(String s,String attribute,Object v,ExecutableContext ctxt) throws RuntimeException, FieldNotSupportedException{
	TreeNode node_value=m_Node.findChild(s);
	if (node_value==null && m_AddIfNotPresent){
	    node_value=new TreeNode(s);
	    m_Node.addChild(node_value);
	}
	if (node_value!=null){
	    if (v!=null){
		if (v instanceof TreeNodeObject){
		    node_value.copyItems(((TreeNodeObject)v).getNode());
		}
		node_value.data(v.toString());
	    }
	}else
	    super.setValue(s,attribute,v,ctxt);
    }
    /**
     * this method stores a value in the in-memory treenode object. If the value is of treenode type, the treenode is first copied and then stored. If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present with a blank label
     * @param array_index the index of the item to update
     * @param v the value itself
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     */
    public void setValueAt(Object array_index,String attribute,Object v,ExecutableContext ctxt) throws RuntimeException{
	int index=Interpreter.intValue(array_index);
	TreeNode node_value=null;
	if (index<m_Node.numChildren())
	    m_Node.nthChild(index);
	if (node_value==null && m_AddIfNotPresent){
	    if (index>=m_Node.numChildren()){
		int num_to_add=index-m_Node.numChildren()+1;
		for (int i=0;i<num_to_add;i++){
		    node_value=new TreeNode("");
		    m_Node.addChild(node_value);
		}
	    }
	}
	if (node_value!=null){
	    if (v!=null){
		if (v instanceof TreeNodeObject)
		    node_value.copyItems(((TreeNodeObject)v).getNode());
		node_value.data(v.toString());
	    }
	}else
	    super.setValueAt(array_index,attribute,v,ctxt);
    }
    /**
     * this method retrieves a value from the in-memory treenode object. If the value retrieved is a treenode type, it is *not* copied. If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present with a blank label
     * @param array_index the name of the value
     * @return the value or null
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     */
    public Object getValueAt(Object array_index,String attribute,ExecutableContext ctxt) throws RuntimeException{
	Object ret=null;
	int index=Interpreter.intValue(array_index);
	TreeNode node_value=null;
	if (index<m_Node.numChildren())
	    node_value=m_Node.nthChild(index);
	if (node_value==null && m_AddIfNotPresent){
	    if (index>=m_Node.numChildren()){
		int num_to_add=index-m_Node.numChildren()+1;
		for (int i=0;i<num_to_add;i++){
		    node_value=new TreeNode("");
		    m_Node.addChild(node_value);
		}
	    }
	}
	if (node_value!=null){
	    ret=createTreeNodeObject(Utils.addStrings(m_Location,"[",array_index.toString(),"]"),node_value);
	}else
	    ret=super.getValueAt(array_index,attribute,ctxt);
	return ret;
    }
    /**
     * this method retrieves a value from the in-memory treenode object. If the value retrieved is a treenode type, it is *not* copied. If the m_AddIfNotPresent flag is true, a new item will be added if one is not already present
     * @param s the name of the value
     * @return the value or null
     * @throws RuntimeException - if there was a problem running the script (such as not having permission to access a field)
     * @throws FieldNotSupportedException - if the field could not be found
     */
    public Object getValue(String s,String attribute,ExecutableContext ctxt) throws RuntimeException, FieldNotSupportedException{
	Object ret=null;
	if (m_Node!=null){
	    TreeNode node_value=m_Node.findChild(s);
	    if (node_value==null && m_AddIfNotPresent){
		node_value=new TreeNode(s);
		m_Node.addChild(node_value);
	    }
	    if (node_value==null && m_AddIfNotPresent){
		node_value=new TreeNode(s);
		m_Node.addChild(node_value);
	    }
	    if (node_value!=null){
		ret=createTreeNodeObject(Utils.addStrings(m_Location,":",s),node_value);
	    }else
		ret=super.getValue(s,attribute,ctxt);
	}
	return ret;
    }
    /**
     * This method returns an object which can iterate over all the immediate child node of this node
     */
    public TreeNodeObjectEnumerator enumerate(){
	return new TreeNodeObjectEnumerator(this);
    }
    /**
     * This method returns an object which can iterate over all the immediate child nodes of this node whose label matches the one given
     */
    public TreeNodeObjectEnumerator enumerate(String label){
	return new TreeNodeObjectEnumerator(this,label);
    }
    /**
     * This method writes the entire node tree to stdout
     * @param interp - interpreter to use for tracing
     */
    public void dump(Interpreter interp){
	try{
	    StringWriter out=new StringWriter();
	    m_Node.write(out,true);
	    interp.trace(out.toString());
	}catch(Exception e){
	    Tracer.trace(e);
	}
    }
    /**
     * this method attempts to find the named method within the in-memory treenode object. If it is found, the Interpreter is used to execute the method
     * @param s the name of the method
     * @param args an array of arguments to the method
     * @param ret the object to receive the result of the method call
     * @param interp interpreter
     * @return true if the method was found, false otherwise
     * @throws ParseException if the code contained syntax errors
     * @throws MethodNotSupportedException if the method could not be found
     * @throws RuntimeException if there was an error running the code
     */
    public Object method(String s,Object[] args,ExecutableContext ctxt) throws ParseException, RuntimeException,MethodNotSupportedException{
	Object ret=null;
	if (m_Node!=null){
	    if (s!=null && s.equals("dump")){
		dump(ctxt.getInterpreter());
	    }else if (s!=null && s.equals("numChildren")){
		ret=new Integer(numChildren());
	    }else if (s!=null && s.equals("addChild")){
		if (args!=null && args.length==1){
		    if (args[0] instanceof TreeNodeObject){
			getNode().addChild(((TreeNodeObject)args[0]).getNode());
		    }else
			Interpreter.runtimeError(ctxt,"TreeNodeObject should be passed to addChild method");
		}else
		    Interpreter.runtimeError(ctxt,"Wrong number of params to addChild method");
	    }else if (s!=null && s.equals("removeChild")){
		if (args!=null && args.length==1){
		    if (args[0] instanceof TreeNodeObject){
			getNode().removeChild(((TreeNodeObject)args[0]).getNode());
		    }else
			Interpreter.runtimeError(ctxt,"TreeNodeObject should be passed to removeChild method");
		}else
		    Interpreter.runtimeError(ctxt,"Wrong number of params to removeChild method");
	    }else{
		ParseNode parseNode=null;
		if (m_MethodCache!=null)
		    parseNode=(ParseNode)m_MethodCache.get(s);
		if (parseNode==null){
		    TreeNode node=m_Node.findChild(s);
		    if (node!=null){
			String code=node.data();
			String location=Utils.addStrings(m_Location,":",s);
			// extract the params etc..
			int first_bracket=code.indexOf('(');
			if (first_bracket!=-1){
			    int next_bracket=code.indexOf(')');
			    if (next_bracket!=-1 && next_bracket>first_bracket){
				int first_brace=code.indexOf('{');
				if (first_brace!=-1 && first_brace>next_bracket){
				    Vector paramList=new Vector();
				    if (next_bracket>first_bracket+1){
					// parse the parameters between the brackets
					String params=code.substring(first_bracket+1,next_bracket);
					if (params.length()>0){
					    StringTokenizer tokenizer=new StringTokenizer(params,", ");
					    while (tokenizer.hasMoreTokens()){
						String param=tokenizer.nextToken();
						if (param.length()>0)
						    paramList.addElement(param);
					    }
					}
				    }
				    code=code.substring(first_brace+1);
				    code=Utils.removeInitialBlankLines(code);
				    ExecuteResult result=ctxt.getInterpreter().executeStringExternalParams(location,this,paramList,code,args,ctxt);
				    ret=result.getResult();
				    parseNode=result.getParseTree();
				    if (m_MethodCache==null)
					m_MethodCache=new Hashtable();
				    m_MethodCache.put(s,parseNode);
				}else
				    ret=super.method(s,args,ctxt);
			    }else
				ret=super.method(s,args,ctxt);
			}else
			    ret=super.method(s,args,ctxt);
		    }else
			ret=super.method(s,args,ctxt);
		}else{	
		    String location=Utils.addStrings(m_Location,":",s);
		    ret=ctxt.getInterpreter().executeParseTree(location,this,parseNode,args,ctxt);
		}
	    }
	}else
	    ret=super.method(s,args,ctxt);
	return ret;
    }
    /** loads the treenode from the given stream 
     * @param location the location of the node
     * @param in the input stream containing the node
     */
    public void load(String location,InputStream stream) throws IOException{
	m_Location=location;
	setNode(new TreeNodeReader(stream).read());
    }
    /** saves the treenode into the given stream 
     * @param out the stream to write to
     * @param compiled whether to store the treenode in compiled format
     */
    public void save(OutputStream out,boolean compiled) throws IOException{
	TreeNode node=getNode();
	if (node!=null)
	    node.write(out,compiled,true);
    }
    /**
     * This method creates a new TreeNodeObject object to wrap a treenode. Override this for special behaviour in derived classes. In this method, the newly created object inherits this object's m_AddIfNotPresent flag
     */
    protected TreeNodeObject createTreeNodeObject(String location,TreeNode node){
	TreeNodeObject object=new TreeNodeObject(location,node);
	object.setAddIfNotPresent(getAddIfNotPresent());
	return object;
    }
    /** sets the flag controlling whether new elements are created as they are accessed
     * @param enable enables this feature (which by default is disabled)
     */
    public void setAddIfNotPresent(boolean enable){
	m_AddIfNotPresent=enable;
    }
    /** this returns the value of the flag controlling whether new elements are created as they are accessed 
     * @return true if the feature is enabled, otherwise false (the default)
     */
    public boolean getAddIfNotPresent(){
	return m_AddIfNotPresent;
    }
    /** this method adds a new child node with the given name and data */
    public TreeNodeObject addNode(String label,String data){
	TreeNode node=new TreeNode(label,data);
	m_Node.addChild(node);
	return createTreeNodeObject(Utils.addStrings(m_Location,label),node);
    }
    /** this method removes the first child node with the given label - it returns true if successfull*/
    public boolean removeNode(String label){
	boolean found=false;
	TreeNode child=m_Node.findChild(label);
	if (child!=null){
	    m_Node.removeChild(child);
	    found=true;
	}
	return found;
    }
    /** this method finds a new child node with the given name and data 
     * @param label the label to match
     * @param data the data to match
     * @return a new TreeNodeObject wrapping the node found, or null
     */
    public TreeNodeObject findChild(String label,String data){
	TreeNodeObject found=null;
	TreeNode child=m_Node.findChild(label,data);
	if (child!=null)
	    found=createTreeNodeObject(Utils.addStrings(m_Location,label),child);
	return found;
    }
    /** this method finds a new child node with the given name 
     * @param label the label to match
     * @return a new TreeNodeObject wrapping the node found, or null
     */
    public TreeNodeObject findChild(String label){
	TreeNodeObject found=null;
	TreeNode child=m_Node.findChild(label);
	if (child!=null)
	    found=createTreeNodeObject(Utils.addStrings(m_Location,label),child);
	return found;
    }
    /** this method finds the nth child node 
     * @param index the index to find
     * @return a new TreeNodeObject wrapping the node found, or null
     */
    public TreeNodeObject nthChild(int index){
	TreeNodeObject found=null;
	TreeNode child=m_Node.nthChild(index);
	if (child!=null)
	    found=createTreeNodeObject(Utils.addStrings(m_Location,"[",new Integer(index).toString(),"]"),child);
	return found;
    }
    /** this method returns true if the node contains a child with the given label */
    public boolean containsNode(String label){
	return m_Node.findChild(label)!=null;
    }
    /**
     * returns the label for the underlying node 
     */
    public String label(){
	return m_Node.label();
    }
    /**
     * returns the data for the underlying node 
     */
    public String data(){
	return m_Node.data();
    }
    /** returns the location of this node */
    public String getLocation(){
	return m_Location;
    }
    /** This method returns a TreeNodeObjectEnumerator object which is used in a foreach statement. This version iterates unconditionally over all the children of this node.
     * @return a new iterator object, or null if this object cannot be iterated
     */
    public ExecutableIterator createIterator(){
	return new TreeNodeObjectEnumerator(this);
    }
    /** This method returns a TreeNodeObjectEnumerator object which is used in a foreach statement. This version is iterates over the child nodes with the matching label.
     * @param qualifier - a string, children with a matching label will be returned
     * @return a new iterator object, or null if this object cannot be iterated
     */
    public ExecutableIterator createIterator(String qualifier){
	return new TreeNodeObjectEnumerator(this,qualifier);
    }
    /**
     * This method changes the location associated with this object
     */
    public void setLocation(String location){
	m_Location=location;
    }
    /**
     * This method returns the source for a scripted method described by the location. 
     */
    public String getSource(String location){
	String src="";
	if (m_Node!=null){
	    String method_name=location;
	    int last_colon=location.lastIndexOf(':');
	    if (last_colon!=-1)
		method_name=location.substring(last_colon+1);
	    TreeNode child=m_Node.findChild(method_name);
	    if (child!=null){
		src=child.data();
		// strip out the parameter and opening/closing braces
		int first_brace=src.indexOf('{');
		if (first_brace!=-1)
		    src=src.substring(first_brace+1);
		int last_brace=src.lastIndexOf('}');
		if (last_brace!=-1)
		    src=src.substring(0,last_brace);
		src=Utils.removeInitialBlankLines(src);
	    }
	}
	return src;
    }
    /**
     * This method returns the instance variables for this object. This is a hashtable mapping name to TreeNodeObject.
     * If names are duplicated the last occurrence will appear in the table
     */
    public Hashtable getInstanceVariables(){
	Hashtable hash=new Hashtable();
	if (m_Node!=null){
	    TreeNodeIterator iter=new TreeNodeIterator(m_Node);
	    while (iter.hasMoreElements()){
		TreeNode node=iter.nextElement();
		hash.put(node.label(),new TreeNodeObject(node.label(),node));
	    }
	}
	return hash;
    }
}
